#pragma once

#include <lua.hpp>

#include <concepts>
#include <string>
#include <type_traits>

namespace division_engine::lua
{
template<typename T>
concept SupportedLuaType =
    std::floating_point<T> || std::same_as<T, bool> || std::convertible_to<T, long> ||
    std::same_as<T, void*> || std::convertible_to<T, std::string> ||
    std::same_as<T, lua_CFunction> || std::is_void<T>();

class State
{
public:
    State(lua_State* c_state)
      : _state(c_state)
    {
    }

    lua_State* c_state() { return _state; }

    template<typename TValue>
    TValue* get_global_ptr(const std::string& name)
    {
        return static_cast<TValue*>(get_global<void*>(name));
    }

    template<typename TValue>
    void set_global_ptr(const std::string& name, TValue* ptr)
    {
        set_global(name, static_cast<void*>(ptr));
    }

    template<SupportedLuaType TValue>
    TValue get_global(const std::string& name)
    {
        lua_getglobal(_state, name.c_str());
        return pop_value<TValue>(_state);
    }

    template<SupportedLuaType TValue>
    void set_global(const std::string& name, TValue value)
    {
        push_value(_state, value);
        lua_setglobal(_state, name.c_str());
    }

    void set_global_func(const std::string& name, lua_CFunction func_ptr)
    {
        set_global(name, func_ptr);
    }

    template<auto Func>
    void set_generated_global_func(const std::string& name)
    {
        using func_t = decltype(Func);
        using signature_t = lua_function_signature<func_t>;
        lua_CFunction ptr = signature_t::template function_pointer<Func>;
        set_global(name, ptr);
    }

private:
    lua_State* _state;

    template<typename T>
    struct lua_function_signature;

    template<SupportedLuaType TReturn, SupportedLuaType... TArgs>
    struct lua_function_signature<TReturn (*)(TArgs...)>
    {
        using function_type = TReturn(TArgs...);

        template<function_type Func>
        static int function_pointer(lua_State* state)
        {
            if constexpr (!std::is_same<TReturn, void>())
            {
                push_value(state, Func(pop_value<TArgs>(state)...));
                return 1;
            }
            else
            {
                Func(pop_value<TArgs>(state)...);
                return 0;
            }
        }
    };

    template<SupportedLuaType TReturn>
    static inline TReturn pop_value(lua_State* state)
    {
        const auto& val = peek_value<TReturn>(state);
        pop(state);
        return val;
    }

    template<SupportedLuaType TReturn>
    static inline TReturn peek_value(lua_State* state)
    {
        if constexpr (std::floating_point<TReturn>)
        {
            return static_cast<TReturn>(lua_tonumber(state, -1));
        }
        else if constexpr (std::same_as<TReturn, bool>)
        {
            return lua_toboolean(state, -1);
        }
        else if constexpr (std::same_as<TReturn, void*>)
        {
            return lua_touserdata(state, -1);
        }
        else if constexpr (std::convertible_to<TReturn, long>)
        {
            return static_cast<TReturn>(lua_tointeger(state, -1));
        }
        else if constexpr (std::convertible_to<TReturn, std::string>)
        {
            size_t len = 0;
            const char* str = lua_tolstring(state, -1, &len);

            return static_cast<TReturn>(std::string(str, len));
        }
        else if constexpr (std::same_as<TReturn, lua_CFunction>)
        {
            return lua_tocfunction(state, -1);
        }
    }

    template<SupportedLuaType TVal>
    static inline void push_value(lua_State* state, TVal value)
    {
        if constexpr (std::floating_point<TVal>)
        {
            lua_pushnumber(state, static_cast<double>(value));
        }
        else if constexpr (std::same_as<TVal, bool>)
        {
            lua_pushbool(state, value);
        }
        else if constexpr (std::same_as<TVal, void*>)
        {
            lua_pushlightuserdata(state, value);
        }
        else if constexpr (std::convertible_to<TVal, long>)
        {
            lua_pushinteger(state, static_cast<long>(value));
        }
        else if constexpr (std::convertible_to<TVal, std::string>)
        {
            const auto& str = static_cast<const std::string&>(value);
            lua_pushlstring(state, str.c_str(), str.size());
        }
        else if constexpr (std::same_as<TVal, lua_CFunction>)
        {
            lua_pushcfunction(state, value);
        }
    }

    static inline void pop(lua_State* state) { lua_remove(state, -1); }
};
}